<?php
 /**
  * Clase para conectar con MySQL con la extensión mysqli
  * Requiere LIGA 2.0 o superior
  * Autor: Ing. Oscar Galileo García García
  * Creación: 3 de Marzo del 2012
  * Última modificación: 4 de Marzo del 2012
  * 
  */
 class MySQL2 {
    private $cnx;
    private $base='';
    function __construct($s,$u,$p,$b) {
        $this->cnx = new mysqli($s,$u,$p); # MySQLi
        if($b!='') {
            $this->seleccionar($b);
        }
    }
    // Función interna que regresa un link de consulta válido
    private function consulta($sql) {
        return $this->cnx->query($sql);  # MySQLi
    }
    // Realiza una consulta de modificación y regresa el número de registros afectados o algún mensaje de error
    private function modificar($sql) {
        $resp = $this->consulta($sql);
        if($this->cnx->affected_rows>0) { # MySQLi
            return $this->cnx->affected_rows; # MySQLi
        }
	return $this->cnx->error; # MySQLi
    }
    // Si una cadena contiene caracteres especiales la convierte a UTF8, sino la regresa igual
    private function regresaUTF8($str) {
        $utf8 = false;
        for($i=0;$i<strlen($str);$i++) {
	 $c = ord($str[$i]);
	 if($c>128) {
	  if($c>247) {$utf8 = true; break;}
	  elseif($c>239) $bytes = 4;
	  elseif($c>223) $bytes = 3;
	  elseif($c>191) $bytes = 2;
	  else {$utf8 = true; break;}
	  if(($i + $bytes) > strlen($str)) {$utf8 = true; break;}
	  while($bytes > 1) {
	   $i++;
	   $b = ord($str[$i]);
	   if($b < 128 || $b > 191) {$utf8 = true; break;}
	   $bytes--;
	  }
	 }
	}
	return !$utf8 ? $str : utf8_encode($str);
    }
    // Se le pasa un arreglo ($tupla) y regresa el mismo arreglo con las cadenas procesadas por regresaUTF8()
    private function arrayRegresaUTF8($arreglo) {
	foreach($arreglo as $k => $v) {
	    $arreglo[$this->regresaUTF8($k)] = $this->regresaUTF8($v);
	}
	return $arreglo;
    }
    // Función que regresa true si alguna de las agujas del arreglo están en el pajar
    private function strpos_array($haystack, $needles) {
        foreach($needles as $str) {
            $pos = strpos($haystack, $str);
            if($pos !== false) {
                return true;
            }
        }
        return false;
    }
    // Indica si una cadena contiene una consulta SQL [0,$cad] o el nombre de una tabla [1,$cad] o schema.tabla [2,schema,tabla]
    private function procesar($cad) {
        $sqls = array('select ','where ','insert ','delete ','update ','order ','group ','inner ','create ','drop ','alter ','grant ','revoke ','commit ','rollback ');
        $cad = trim($cad);
        if($this->strpos_array($cad,$sqls)) {
            return array(0,$cad);
        } elseif(strpos($cad,'.')!==false) {
            $cad = explode('.',$cad);
            return array(2,$cad[0],$cad[1]);
        } else {
            return array(1,$cad);
        }
    }
    // Obtiene una cadena where a partir de un array
    private function array2where($cad2,$cad3,$tabla='') {
     // Se convierte en una consulta válida
     if(is_array($cad2) && count($cad2)>0) {
      // Si cad2 es un array se arma un where
      $cad1 = '';
      $tab = null;
      if($tabla!='') {
       $tab = new LIGA($tabla);
      }
      foreach($cad2 as $k=>$v) {
       if($tab && $tab->col_existe($k)) {
        if ($tab->col_prop($k, 'pri')) {
	 $cad1 .= ($cad1=='') ? ((is_string($k) && $v) ? "where `".$this->cnx->real_escape_string($k)."` = '".$this->cnx->real_escape_string($k)."'" : '') : ((is_string($k) && $v) ? " and `".$this->cnx->real_escape_string($k)."` = '".$this->cnx->real_escape_string($k)."'" : '') ;
	} else {
	 $cad1 .= ($cad1=='') ? ((is_string($k) && $v) ? "where `".$this->cnx->real_escape_string($k)."` like '%".$this->cnx->real_escape_string($k)."%'" : '') : ((is_string($k) && $v) ? " and `".$this->cnx->real_escape_string($k)."` like '%".$this->cnx->real_escape_string($k)."%'" : '') ;
	}
       }
      }
      $cad2 = $cad1.' '.$cad3;
     }
     return is_array($cad2) && count($cad2)==0 ? ' '.$cad3 : $cad2;
    }
    // Obtiene la meta información de una tabla o consulta (sin datos)
    public function metaInfo($cad) {
        // Para no convertir los datos insertados entre codidificaciones
        $this->consulta('SET NAMES \'utf8\''); # MySQLi
        $acc = $this->procesar($cad);
        // Transformamos la cadena en una consulta válida para LIGA
        if($acc[0]==0) {
            // Consulta SQL
            $limit = strpos(strtolower($acc[1]),'limit');
            if($limit!==false) {
                // Posee un limit, hay que modificarlo
                $cad = substr($acc[1],0,$limit).'LIMIT 0';
            }
        } elseif($acc[0]==1) {
            // Sólo el nombre de la tabla (checar que esté seleccionado un Schema)
            if($this->seleccionada()) {
                $cad = "select * from `$acc[1]` LIMIT 0";
            } else {
                throw new Error('Error al intentar ejecutar la consulta hacia la tabla ($cad) debido a que no se seleccionó un Schema válido');
            }
        } else {
            // Nombre de tabla y el Schema
            $cad = "select * from `$acc[1]`.`$acc[2]` LIMIT 0";
            $this->seleccionar($acc[1]);
        }
        // Se realiza la consulta
        $resp = $this->consulta($cad);
        // Obtenemos cada campo con su meta información
        $campos = array();
        // Guardo la información útil que existe en el objeto
        while($campo = $resp->fetch_field()) {  # MySQLi
            $null = ($campo->flags & 1) ? false : true; # MySQLi
            $num = ($campo->flags & 32768) ? true : false; # MySQLi
            $blob = ($campo->flags & 16) ? true : false; # MySQLi
	    $tipo = 'integer';
	    $tipo = ($campo->flags & 4 || $campo->flags & 5 || $campo->flags & 246) ? 'decimal' : $tipo;
	    $tipo = ($campo->flags & 10) ? 'date' : $tipo;
	    $tipo = ($campo->flags & 12) ? 'datetime' : $tipo;
	    $tipo = ($campo->flags & 7) ? 'timestamp' : $tipo;
	    $tipo = ($campo->flags & 11) ? 'time' : $tipo;
	    $tipo = ($campo->flags & 252 || $campo->flags & 253 || $campo->flags & 254) ? 'string' : $tipo;
            $campos[$campo->name] = array('tabla'=>$campo->table,'null'=>$null,'num'=>$num,'blob'=>$blob,'tipo'=>$tipo,'pri'=>false,'ai'=>false,'codif'=>false,'com'=>false,'max'=>0); # MySQL
        }
        // Ahora verifico información extra de las columnas, como las llaves primarias y los auto_increment
        if($this->seleccionada()) {
            try {
                foreach($campos as $nom => $info) {
                    $resp = $this->consulta("show full columns from `$info[tabla]` like '$nom'");  # MySQLi
                    $resp = $resp->fetch_assoc();  # MySQLi
                    $cc = $resp['Collation']; # MySQL
                    $com = $resp['Comment']; # MySQL
                    $key = ($resp['Key']==='PRI') ? true : false; # MySQL
                    $ai = ($resp['Extra']==='auto_increment') ? true : false; # MySQL
                    $campos[$nom]['codif'] = $cc;
                    $campos[$nom]['pri'] = $key;
                    $campos[$nom]['ai'] = $ai;
		    // Los que pueden ser nulos
		    $campos[$nom]['nulo'] = $resp['Null']==='NO' && $resp['Default']===null && !$ai ? false : true;
                    $campos[$nom]['com'] = $com;
                    // Para obtener un tipo alterno con todo y su maxlegth (No se usará por métodos oficiales de LIGA)
                    $tipo = $resp['Type']; # MySQL
                    $campos[$nom]['type'] = $tipo; # MySQL
		    // Si el tipo es timestamp no es numerico...
		    $campos[$nom]['num'] = $tipo==='timestamp' ? false : $campos[$nom]['num']; # MySQL
                    // Para guardar el maxlength si está declarado (MySQL)
                    if(strpos($tipo,'(')!==false) {
                        $ini = strpos($tipo,'(')+1;
                        $fin = strpos($tipo,')');
                        $max = substr($tipo,$ini,$fin-$ini);
                        $campos[$nom]['max'] = $max;
                    }
		    // Soporte para las llaves foráneas
		    $resp = $this->consulta("SELECT CONCAT(REFERENCED_TABLE_SCHEMA,'.',referenced_table_name,'::',referenced_column_name) AS foranea FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = '".$this->seleccionada()."' AND table_name = '$info[tabla]' AND column_name = '$nom' AND REFERENCED_TABLE_NAME is not null");
		    if($resp->num_rows==1) { # MySQLi
		     $ref = $resp->fetch_assoc(); # MySQLi
		     $campos[$nom]['referencia'] = $ref['foranea'];
		    }
                }
            } catch(Error $e) {
                // Si no se puede ejecutar la consulta no se dispara el error
            }
        }
        return $campos;
    }
    // Obtiene la información de la tabla acorde a las columnas consultadas en la metaInfo()
    public function info($cad,$cad2='',$cad3='') {
        $this->consulta('SET NAMES \'utf8\''); # MySQLi
	// Se convierte en una consulta válida
	$acc = $this->procesar($cad);
	$sql;
	if($acc[0]==0) {
	    $cad2 = $this->array2where($cad2,$cad3);
	    $sql = $cad.$cad2;
	} else {
	    $cad2 = $this->array2where($cad2,$cad3,$cad);
	    $sql = "select * from $cad $cad2";
	}
	// Se realiza la consulta y se atrae la información
	$resp = $this->consulta($sql);
	$info['numReg'] = $resp->num_rows;  # MySQLi
	$info['numCol'] = $resp->field_count;  # MySQLi
	while($tupla = $resp->fetch_assoc()) {  # MySQL
	    $info['info'][] = $this->arrayRegresaUTF8($tupla);
	}
	$resp->free();  # MySQLi
	return $info;
    }
    // Seleccionar un Schema por defecto
    public function seleccionar($b) {
        if(is_string($b) && $b!='') {
            $resp = $this->cnx->select_db($b);  # MySQLi
            if($resp) {
                $this->base = $b;
            } else {
                throw new Error("Es Schema ($b) no es válido");  # MySQL
            }
        } else {
            throw new Error('No estableció un Schema válido');
        }
    }
    // Obtiene el Schema por defecto seleccionado o false si ninguno
    public function seleccionada() {
        if($this->base=='') {
            return false;
        } else {
            return $this->base;
        }
    }
    // Valida, arma y envía consulta de inserción, regresando la cantidad de registros modificados o los errores de validación encontrados
    public function insertar($datos) {
        // Para armar una consulta optimizada con todos los registros
        $sql = '';
        foreach($datos as $fila) {
            // Guardo la tabla donde se almacena el campo (por el momento deberá ser la misma para todos)
            $tabla = array_keys($fila);
            $tabla = $tabla[0];
            // Guardo las llaves (columnas)
            $llaves = array_keys($fila[$tabla]);
            // Se limpian las variables de inyecciones de SQL
            $valores = array();
            foreach($llaves as $k) {
                if($fila[$tabla][$k]['num']) {
                    $valores[] = $this->cnx->real_escape_string($fila[$tabla][$k]['dato']); # MySQLi
                } else {
                    // Al parecer la codificación es irrelevante para MySQL, para pruebas funciona bien así como está :D
                    $valores[] = "'".$this->cnx->real_escape_string($fila[$tabla][$k]['dato'])."'"; # MySQLi
                }
            }
            // Si $sql está vacío se arman las cabeceras
            $valores = implode(',',$valores);
            if($sql=='') {
                $llaves = '`'.implode('`,`',$llaves).'`';
                $sql = "insert into ".$this->seleccionada().'.'.$tabla." ($llaves) values ($valores)";
            } else {
                $sql .= ",($valores)";
            }
        }
        return $this->modificar($sql);
    }
    // Arma y envía consulta de actualización, regresando la cantidad de registros modificados o los errores de validación encontrados
    public function actualizar($datos,$tabla,$llave) {
        if(count($datos)==0) {
            return 0;
        }
        $res = $this->procesar($tabla);
        if($res[0]>0) {
            $tabla = ($res[0]==1) ? $res[1] : $res[2];
            $llave = $llave[$tabla][0];
            $act = 0;
	    $err = '';
            foreach($datos as $k => $dats) {
                $sql = '';
                foreach($dats as $col => $val) {
		    $val = (is_numeric($val)) ? "'$val'" : "'$val'";
		    $val = ($val=="'[null]'") ? 'null' : $val;
		    $sql .= ($sql=='') ? "update `$tabla` set `$col` =$val" : ",`$col`=$val";
                }
		$sql .= (is_numeric($k)) ? " where `$llave` = $k" : (strpos($k,'where')===false) ? " where `$llave` = '$k'":" $k";
		$resp = $this->modificar($sql);
                if(is_numeric($resp)) {
                    $act++;
                } else {
		    $err .= $resp.'. ';
		}
            }
	    return ($act>0) ? intval(trim($act.' '.$err)) : $err;
        } else {
            return 0;
        }
    }
    // Arma y envía consulta de borrado, regresando la cantidad de registros modificados o los errores de validación encontrados
    public function eliminar($datos,$tabla,$llave) {
        if(count($datos)==0) {
            return 0;
        }
        $res = $this->procesar($tabla);
        if($res[0]>0) {
	    $tabla;
            if($res[0]==1) {
                $tabla = $res[1];
            } else {
                $tabla = $res[2];
            }
            $llaves = '';
            foreach($datos as $dato) {
	     if(strpos($dato,'where')===false) {
	      $dato = $this->cnx->real_escape_string($dato); # MySQLi
                if($llaves=='') {
                    if(is_numeric($dato)) {
                        $llaves .= $dato;
                    } else {
                        $llaves .= "'".$dato."'";
                    }
                } else {
                    if(is_numeric($dato)) {
                        $llaves .= ",".$dato;
                    } else {
                        $llaves .= ",'".$dato."'";
                    }
                }
	     } else {
	      $this->modificar("delete from `$tabla` $dato");
	     }
            }
	    if($llaves) {
	     $llave = $llave[$tabla][0];
	     $sql = "delete from `$tabla` where `$llave` in ($llaves)";
	     return $this->modificar($sql);
	    }
        } else {
            return 0;
        }
    }
 }
?>